package meatslope2.gameengine.engine;

import meatslope2.constants.BitmapConstants;
import meatslope2.constants.TileConstants;
import meatslope2.gameengine.gameobjects.Tile;
import meatslope2.gameengine.helpers.Utils;
import meatslope2.gameengine.primatives.HitInfo;
import meatslope2.gameengine.primatives.LineSegment;
import meatslope2.gameengine.primatives.Vector2;
import meatslope2.gameengine.scene.TileWorld;
import android.util.FloatMath;

/**
 * Has a reference to the tile world, can test incoming rays against this world
 * @author Mitchell Thelen
 *
 */
public class CollisionSystem
{
	// ===========================================================
	// Members
	// ===========================================================
	private final TileWorld tileWorld;
	private HitInfo currentHitInfo = new HitInfo();
	
	private final int worldWidth;
	private final int worldHeight;
	
	private TileVisitor tileVisitor = new TileVisitor();
	
	// ===========================================================
	// Constructors
	// ===========================================================
	public CollisionSystem(TileWorld tWorld)
	{
		this.tileWorld = tWorld;
		
        this.worldWidth = tileWorld.getPixelWidth();
        this.worldHeight = tileWorld.getPixelHeight();
	}
	
	// ===========================================================
	// Methods
	// ===========================================================
	
	/**
	 * Sees if the incoming coordinates have intersected with a tile in the world, will return this class's hitInfo member
	 */
    public final HitInfo castRay(Vector2 startPoint, Vector2 endPoint) 
    {        
        final int startTileX = worldToTileColumn(startPoint.x, worldWidth);
        final int startTileY = worldToTileRow(startPoint.y, worldHeight);
        
        final int endTileX = worldToTileColumn(endPoint.x, worldWidth);
        final int endTileY = worldToTileRow(endPoint.y, worldHeight);
        
        final int deltaX = endTileX - startTileX;
        final int deltaY = endTileY - startTileY;
        
        currentHitInfo.reset();
        
        //Test the tile we're currently in
        if (deltaX == 0 && deltaY == 0)
        {
        	testCurrentTile(startPoint, endPoint, 
        			startTileX, startTileY);
        }
        //Test across tiles vertically or horizontally
        else if (deltaX == 0 || deltaY == 0)
        {
        	executeStraightRay(startPoint, endPoint, 
        			startTileX, startTileY, 
        			endTileX, endTileY, 
        			deltaX, deltaY);
        }
        //Test a ray in any direction across multiple tiles
        else
        {
        	runBresenhamLineAlgo(startPoint, endPoint,
        			startTileX, startTileY,
        			endTileX, endTileY,
        			deltaX, deltaY);
        }
        
        return currentHitInfo;
    }
    
    /**
     * ganked from http://tech-algorithm.com/articles/drawing-line-using-bresenham-algorithm/
     */
    private final void runBresenhamLineAlgo(Vector2 startPoint, Vector2 endPoint,
    										 int x, int y,
    										 int x2, int y2,
    										 int w, int h) 
    {
	    int dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0 ;
	    if (w<0) dx1 = -1 ; else if (w>0) dx1 = 1 ;
	    if (h<0) dy1 = -1 ; else if (h>0) dy1 = 1 ;
	    if (w<0) dx2 = -1 ; else if (w>0) dx2 = 1 ;
	    
	    int longest = Math.abs(w);
	    int shortest = Math.abs(h);
	    
	    if (!(longest > shortest)) 
	    {
	        longest = Math.abs(h) ;
	        shortest = Math.abs(w) ;
	        if (h<0) dy2 = -1 ; else if (h>0) dy2 = 1 ;
	        dx2 = 0 ;            
	    }
	    
	    int numerator = longest >> 1 ;
	    
	    for (int i=0;i<=longest;i++) 
	    {
            final int tileIndex = tileWorld.getTile(x, y);
            if(tileIndex != TileConstants.NOT_SET)
            {
            	Tile currentTile = TileConstants.getTile(tileIndex);
                currentHitInfo = tileVisitor.collides(currentTile, startPoint, endPoint, x, y);
                if(currentHitInfo.isHit())
                    break;
            }
            
	        numerator += shortest;
	        
	        if (!(numerator < longest)) 
	        {
	            numerator -= longest ;
	            x += dx1 ;
	            y += dy1 ;
	        } 
	        else 
	        {
	            x += dx2 ;
	            y += dy2 ;
	        }
	    }
	}

	private final void testCurrentTile(Vector2 startPoint, Vector2 endPoint, int tileX, int tileY) 
    {   
    	final int tileIndex = tileWorld.getTile(tileX, tileY);
    	
        if (tileIndex != TileConstants.NOT_SET) 
        {
        	Tile currentTile = TileConstants.getTile(tileIndex);
        	currentHitInfo = tileVisitor.collides(currentTile, startPoint, endPoint, tileX, tileY);
        }
	}

	private final void executeStraightRay(final Vector2 startPoint, final Vector2 endPoint, 
            final int startTileX, final int startTileY, final int endTileX, final int endTileY,
            final int deltaX, final int deltaY) 
    {
        int currentX = startTileX;
        int currentY = startTileY;

        int xIncrement = 0;
        int yIncrement = 0;
        int distance = 0;
        
        if (deltaX != 0)
        {
            distance = Math.abs(deltaX) + 1;
            xIncrement = Utils.sign(deltaX);
        } 
        else if (deltaY != 0) 
        {
            distance = Math.abs(deltaY) + 1;
            yIncrement = Utils.sign(deltaY);
        }
        
        for (int x = 0; x < distance; x++) 
        {
        	final int tileIndex = tileWorld.getTile(currentX, currentY);
            if (tileIndex != TileConstants.NOT_SET) 
            {
            	Tile currentTile = TileConstants.getTile(tileIndex);
            	currentHitInfo = tileVisitor.collides(currentTile, startPoint, endPoint, currentX, currentY);
            	
            	if(currentHitInfo.isHit())
                    break;
            }
            currentX += xIncrement;
            currentY += yIncrement;
        }
    }
    
    private static final int worldToTileColumn(final float x, final int width) 
    {
        return Utils.clamp((int)FloatMath.floor(x / BitmapConstants.TILE_WIDTH), 0, width - 1);
    }
    
    private static final int worldToTileRow(float y, final int height) 
    {
        return Utils.clamp((int)FloatMath.floor(y / BitmapConstants.TILE_HEIGHT), 0, height - 1);
    }
    
    private class TileVisitor 
    {
    	// ===========================================================
    	// Cached Members
    	// ===========================================================
        private final Vector2 tileSpaceStart = new Vector2();
        private final Vector2 tileSpaceEnd = new Vector2();
        private final Vector2 tileSpaceOffset = new Vector2();
        
        //Holds the closest hitInfo information
        private final HitInfo closestHitInfo = new HitInfo();
        
        //Temporary HitInfo object to store intermediate results, pass into closestHitInfo is necessary
        private final HitInfo tempHitInfo = new HitInfo();
        
	    /** 
	     * Converts the ray into tile space and then compares it to the segments
	     * stored in the current tile.
	     */
	    private HitInfo collides(Tile tile, Vector2 startPoint, Vector2 endPoint, int tileX, int tileY) 
	    {	
	        tileSpaceOffset.set(tileX * BitmapConstants.TILE_WIDTH, tileY * BitmapConstants.TILE_HEIGHT);
	        
	        tileSpaceStart.set(startPoint);
	        tileSpaceStart.subtract(tileSpaceOffset);
	        
	        tileSpaceEnd.set(endPoint);
	        tileSpaceEnd.subtract(tileSpaceOffset);
	        
	        closestHitInfo.reset();
	        testSegmentAgainstList(tile.getLineSegments(), tileSpaceStart, tileSpaceEnd);

            // The hitPoint is in tile space, so convert it back to world space.
	        if (closestHitInfo.isHit()) 
	        	closestHitInfo.getHitPoint().add(tileSpaceOffset);
	        
	        return closestHitInfo;
	    }
	    
	    /**
	     * Given a list of segments and a ray, this function performs an intersection search and
	     * returns the closest intersecting segment, if any exists.
	     */
	    private final void testSegmentAgainstList(LineSegment[] segments, Vector2 startPoint, Vector2 endPoint) 
	    {
	        float closestDistance = -1; 
	        for (int i = 0; i < segments.length; i++) 
	        {
	            LineSegment segment = segments[i];
	            
            	tempHitInfo.reset();
        		segment.calculateIntersection(startPoint, endPoint, tempHitInfo);
            	if(!tempHitInfo.isHit())
            		continue;
        	
            	final float distance = tempHitInfo.getHitPoint().distance2(startPoint);

            	//If we haven't found a collision yet, or this is a closer collision - set it to closestHitInfo
            	if (!closestHitInfo.isHit() || closestDistance > distance)
            	{
            		closestHitInfo.set(tempHitInfo);
            		closestDistance = distance;
            	}
            }
	    }
    }
}
